package com.bluesky.javawebbrowser.domain.html.tags;

import java.awt.Color;
import java.awt.FlowLayout;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;

import org.apache.log4j.Logger;

import com.bluesky.common.TreeNode;
import com.bluesky.javawebbrowser.domain.html.HtmlPage;

/**
 * It is a raw tag TODO parse style attribute when set?
 * 
 * @author HZ00260
 * 
 */
public class Tag extends TreeNode {
	private static Logger logger = Logger.getLogger(Tag.class);

	/**
	 * indicate the root tag of a component
	 */
	public static String JWF_ID = "jwfid";
	/**
	 * indicate the sub tag of a component
	 */
	public static String JWF_CLASS = "jwfclass";

	/**
	 * if a tag has a attribute of WJF_INCLUDE and value is 'true'(default is
	 * false), and it is a root tag of a template, it means that root tag(aka.
	 * container tag) is part of the template.
	 */
	public static String JWF_INCLUDE = "jwfinclude";

	public static List<String> nonMonoTagNameList;

	static {

		nonMonoTagNameList = new ArrayList<String>();
		nonMonoTagNameList.add("A");

	}

	Pattern pTagAttribute = Pattern
			.compile("([\\w-_]+)=([\\w-_]+|\".*?\"|'.*?')");

	protected String content;// all html
	protected TagType tagType;
	protected String tagName1;
	protected String head;
	protected String body;
	protected String foot;

	// private String id;

	protected Map<String, String> attributes = new HashMap<String, String>();

	protected Map<String, String> styleAttributes = new HashMap<String, String>();

	public Tag(Tag tag) {
		copy(tag);
		// TODO tackle the child-parent relation.
	}

	public Tag(TagType tagType) {
		this.tagType = tagType;
	}

	public Tag(String head) {

		setHead(head);
	}

	public String getFoot() {
		return foot;
	}

	public void setFoot(String foot) {
		this.foot = foot;
	}

	public String getTagName() {
		if (tagType != null)
			return tagType.toString();
		else
			return tagName1;
	}

	public void setTagName(String name) {
		this.tagName1 = name;

		try {
			tagType = TagType.getType(name.toUpperCase());
		} catch (Exception e) {
			throw new RuntimeException("unknow tag:" + name);
		}
	}

	public TagType getTagType() {
		return tagType;
	}

	public void setTagType(TagType tagType) {
		this.tagType = tagType;
	}

	public String getId() {
		return (String) getAttribute("id");
	}

	public void setId(String id) {
		setAttribute("id", id);
	}

	public String getHead() {
		return head;
	}

	public void setHead(String head) {
		this.head = head;

		setTagName(extractTagName(head));

		// parseName(head);
		// name=extractTagName(head);
		parseAttributes(head);

	}

	private void parseName(String tagHead) {

		Pattern pTagHead = Pattern
				.compile("<[\\s\\t\\r\\n]*(!?\\w+)[\\s\\t\\r\\n]*.*>");

		Matcher m = pTagHead.matcher(tagHead);
		if (m.find())
			setTagName(m.group());
	}

	private void parseStyleAttributes(String style) {
		styleAttributes.clear();

		String[] groups = style.split(";");
		for (String g : groups) {
			// skip empty string
			if (g.matches("^[\\t\\s]*$"))
				continue;

			// eg. color:red,find the first ':'
			int index = g.indexOf(':');
			if(index<0)			
				throw new RuntimeException("fail to parse style:" + style
						+ " at:" + g);

			String name = g.substring(0,index);
			String value = g.substring(index+1);
			styleAttributes.put(name, value);
		}

	}

	private void parseAttributes(String tagHead) {
		attributes.clear();

		Matcher attributeMatcher = pTagAttribute.matcher(tagHead);
		while (attributeMatcher.find()) {

			String name = attributeMatcher.group(1).toLowerCase();
			String value = attributeMatcher.group(2);

			String pureValue = value;
			// strip the "" or '' of the value
			if (value.startsWith("\"") && value.endsWith("\""))
				pureValue = value.substring(1, value.length() - 1);

			if (value.startsWith("'") && value.endsWith("'"))
				pureValue = value.substring(1, value.length() - 1);

			setAttribute(name, pureValue);
		}

	}

	public String getBody() {

		return body;
	}

	/**
	 * re-parse the property 'body',body is stuff between <xx> and </xx>
	 * 
	 */
	public void parseBody() {

		// parse the body as children

		removeAllChildren();
		HtmlPage page = new HtmlPage(body, true);
		if (page.getRoot().getChildren().isEmpty()) {
			if (body != null && !body.equals(""))
				addChild(new TextBlock(body));
		} else {
			for (TreeNode tn : page.getRoot().getChildren()) {
				Tag tag = (Tag) tn;
				addChild(tag);
			}
		}
	}

	/**
	 * the body attribute is useless, except for TextBlock.
	 * 
	 * @param body
	 */
	public void setBody(String body) {
		this.body = body;
	}

	public void setBodyAndParse(String body) {
		setBody(body);
		parseBody();
	}

	public static String indents(int number, String s) {
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < number; i++)
			sb.append(s);

		return sb.toString();
	}

	public static String removeCR(String s) {
		return s.replaceAll("\n", "<CR>");
	}

	public static boolean isTag(String name) {
		if (name.startsWith("<") && name.endsWith(">"))
			return true;
		else
			return false;
	}

	public static String extractTagName(String s) {
		// <a> </a> <a href=abc> <br/>

		s = s.replaceAll("<", "");
		s = s.replaceAll("/", "");
		s = s.replaceAll(">", "");

		String[] ss = s.trim().split("\\s|\\t|\\r|\\n");

		String tagName = ss[0];

		if (tagName.equals("!"))
			tagName = TagType.COMMENT.toString();

		return tagName;
	}

	public static boolean isStartTag(String tag) {
		return isTag(tag) && (tag.charAt(1) != '/');
	}

	public static boolean isEndTagName(String tag) {
		return isTag(tag) && (tag.charAt(1) == '/');
	}

	public static boolean isStartAndEndTagName(String tag) {
		String tagName = extractTagName(tag).toUpperCase();

		return isTag(tag) && tag.endsWith("/>")
				&& nonMonoTagNameList.indexOf(tagName) < 0;
	}

	public boolean isStartAndEndTag() {

		return head.endsWith("/>")
				&& nonMonoTagNameList.indexOf(getTagName()) < 0;
	}

	public static boolean isCommentTag(String tag) {
		return tag.startsWith("<!");
	}

	public boolean isCommentTag() {
		return tagType == TagType.COMMENT;
	}

	public static boolean isPair(String start, String end) {
		// validate
		if (isStartTag(start) && isEndTagName(end)) {

			if (end.substring(2).compareToIgnoreCase(start.substring(1)) == 0)
				return true;

		}
		return false;
	}

	public String getContent() {
		if (content == null)
			return head;
		else
			return content;
	}

	/**
	 * content is all html, include tag head and foot
	 * 
	 * @param content
	 */
	public void setContent(String content) {
		this.content = content;
	}

	/**
	 * re-concatenate style attributes
	 * 
	 * @return
	 */
	public String getStyleAttribute() {
		String result;
		// generate the style attribute from styleAttributes property
		if (!styleAttributes.isEmpty()) {
			StringBuilder sbStyleAttributes = new StringBuilder();
			Iterator<String> itStyleKey = styleAttributes.keySet().iterator();
			while (itStyleKey.hasNext()) {
				String key = itStyleKey.next();
				String value = styleAttributes.get(key);

				sbStyleAttributes.append(" " + key + ":" + value + ";");
			}

			// trim the first Space character.
			result = sbStyleAttributes.toString().trim();
		} else
			result = null;
		return result;
	}

	public boolean containsAttribute(String name) {
		return attributes.containsKey(name);
	}

	public String getAttribute(String name) {
		if (name.equalsIgnoreCase("style"))
			return getStyleAttribute();
		else
			return attributes.get(name.toLowerCase());
	}

	public void setAttribute(String name, String value) {
		if (name.equalsIgnoreCase("style"))
			parseStyleAttributes(value);
		else
			attributes.put(name, value);
	}

	public void setAttribute(String name, String value, boolean removeIfNull) {
		if (value == null)
			removeAttribute(name);
		else
			setAttribute(name, value);
	}

	public void removeAttribute(String name) {
		if (name.equalsIgnoreCase("style"))
			styleAttributes.clear();
		attributes.remove(name);
	}

	public String getStyleAttribute(String name) {
		return styleAttributes.get(name.toLowerCase());
	}

	public void setStyleAttribute(String name, String value) {
		styleAttributes.put(name, value);
	}

	public void removeStyleAttribute(String name) {
		styleAttributes.remove(name);
	}

	public static void main(String[] args) {
		String s = "<body a=\"1\" bb=\"222\" c=444>";
		s = "<div class=\"references-small\" style='margin-left: 1.5em;\' c=\"string s='c'\">";

		Matcher m = Pattern.compile("(\\w+)=(\\w+|\".*?\"|'.*?')").matcher(s);

		// Pattern p = Pattern.compile("((cat)\\w*)");
		// Matcher m = p.matcher("one cat two cats in the yard");
		StringBuffer sb = new StringBuffer();
		while (m.find()) {
			// logger.debug(String.valueOf( m.groupCount()));
			for (int i = 0; i < m.groupCount(); i++)
				sb.append("\n group-" + (i + 1) + ":" + m.group(i + 1));

			sb.append("\nkey:" + m.group(1) + "\n");
			sb.append("  value:" + m.group(2) + "\n");

			// m.appendReplacement(sb, "dog");
		}
		// m.appendTail(sb);
		System.out.println(sb.toString());

		Tag tag = new Tag("<a\nhref='xx'>vv</a>");

		System.out.println(tag.getTagName());

	}

	// private static List<String> NO_ATTRIBUTE_VALUE_TAG_NAMES;
	// static {
	// NO_ATTRIBUTE_VALUE_TAG_NAMES = new ArrayList<String>();
	// NO_ATTRIBUTE_VALUE_TAG_NAMES.add("checked");
	// NO_ATTRIBUTE_VALUE_TAG_NAMES.add("selected");
	//
	// }

	public String getHEAD() {

		// build the head from tag type and attribute
		StringBuilder sb = new StringBuilder();
		sb.append("<" + getTagName().toUpperCase());

		// re-calculate the style attribute
		String styleAttributeValue = getStyleAttribute();
		if (styleAttributeValue != null)
			attributes.put("style", styleAttributeValue);
		else
			removeAttribute("style");

		Iterator<String> itKey = attributes.keySet().iterator();
		while (itKey.hasNext()) {
			String key = itKey.next();
			String value = attributes.get(key);

			if (value == null) {

				sb.append(" " + key);
				// else nothing to output
			} else
				sb.append(" " + key + "=\"" + value + "\"");
		}

		if (tagType.isSolo())
			sb.append("/>");
		else
			sb.append(">");

		return sb.toString();

	}

	public String getFOOT() {
		if (tagType.isSolo())
			return "";

		return "</" + getTagName() + ">";

	}

	public Map<String, String> getAttributes() {
		return attributes;
	}

	public void setAttributes(Map<String, String> attributes) {
		this.attributes = attributes;
	}

	public Map<String, String> getStyleAttributes() {
		return styleAttributes;
	}

	public void setStyleAttributes(Map<String, String> styleAttributes) {
		this.styleAttributes = styleAttributes;
	}

	public JComponent getComponent() {

		if (isText()) {
			JLabel label = new JLabel();
			label.setText(getBody());

			return label;
		} else {
			// is container
			JPanel boxPanel = new JPanel();
			boxPanel.add(new JLabel("<" + getTagName() + ">"));

			boxPanel.setLayout(new FlowLayout());
			boxPanel.setBorder(BorderFactory.createLineBorder(Color.black, 1));

			Iterator<TreeNode> it = getChildren().iterator();
			while (it.hasNext()) {
				Tag child = (Tag) it.next();
				boxPanel.add(child.getComponent());
			}
			return boxPanel;

		}

	}

	@Override
	public String toString() {
		return tagType.toString() + ":" + getHEAD() + "...body..." + getFOOT();

	}

	public void writeDescendantHtml(Writer writer) throws IOException {
		// children
		Iterator<TreeNode> itChildren = getChildren().iterator();

		while (itChildren.hasNext()) {

			Tag tag = (Tag) itChildren.next();
			tag.writeHtml(writer);

		}
	}

	public void writeHtml(Writer writer) throws IOException {
		if (isText())
			writer.write(body);
		else {
			// head
			boolean indent = true;
			if (tagType.isOneTextChildOnly())
				indent = false;

			if (indent)
				writer.write("\n" + Tag.indents(getDepth(), "  "));

			writer.write(getHEAD());

			writeDescendantHtml(writer);

			// end
			if (indent)
				writer.write("\n" + Tag.indents(getDepth(), "  "));

			writer.write(getFOOT());

		}
	}

	public void writeTagTree(Writer writer) throws IOException {
		if (isText())
			writer.write(body);
		else {
			// head
			boolean indent = true;
			if (tagType.isOneTextChildOnly())
				indent = false;

			if (indent)
				writer.write("\n" + Tag.indents(getDepth(), "  "));

			writer.write("<" + getHEAD() + ">");

			writeDescendantHtml(writer);

			// end
			if (indent)
				writer.write("\n" + Tag.indents(getDepth(), "  "));

			writer.write("<" + getFOOT() + ">");

		}
	}

	public String toTagTree() {

		try {
			StringWriter writer = new StringWriter();
			writeTagTree(writer);
			return writer.toString();
		} catch (IOException e) {

			throw new RuntimeException(e);
		}

	}

	public String toHtml() {

		try {
			StringWriter writer = new StringWriter();
			// OutputStreamWriter osw = new OutputStreamWriter()
			writeHtml(writer);
			return writer.toString();
		} catch (IOException e) {

			throw new RuntimeException(e);
		}

	}

	public String descendantToHtml() {

		try {
			StringWriter writer = new StringWriter();
			writeDescendantHtml(writer);
			return writer.toString();
		} catch (IOException e) {

			throw new RuntimeException(e);
		}

	}

	public boolean isText() {

		return tagType == TagType.TEXT_BLOCK;
	}

	/**
	 * clone the whole tree
	 */
	public Tag clone() {
		logger.debug("in clone()" + this);

		Tag newTag = new Tag(getTagType());
		newTag.copy(this);
		newTag = newTag.upgrade();
		// clear child
		newTag.setChildren(new ArrayList<TreeNode>());

		// clone children
		for (TreeNode tn : getChildren()) {
			Tag t = (Tag) tn;
			Tag newChildTag = t.clone();
			newTag.addChild(newChildTag);
		}

		return newTag;
	}

	/**
	 * do not copy its decendant
	 */
	@Override
	public void copy(TreeNode src) {

		logger.debug("in copy()" + src);

		super.copy(src);

		Tag tag = (Tag) src;

		attributes.clear();
		attributes.putAll(tag.getAttributes());

		styleAttributes.clear();
		styleAttributes.putAll(tag.getStyleAttributes());

		content = tag.getContent();
		setTagName(tag.getTagName());
		head = tag.getHead();
		body = tag.getBody();
		foot = tag.getFoot();
		setTagType(tag.getTagType());

	}

	public interface TagProcessor {
		/**
		 * return true if done.
		 * 
		 * @param tag
		 * @return
		 */
		boolean process(Tag tag);
	}

	/**
	 * itself and it's descendant
	 * 
	 * @param tagProcessor
	 * @return
	 */
	public boolean traverse(TagProcessor tagProcessor) {
		boolean b = tagProcessor.process(this);
		if (b == true)// if true, stop traverse
			return true;

		for (TreeNode tn : getChildren()) {
			Tag tag = (Tag) tn;
			b = tag.traverse(tagProcessor);
			if (b == true)// if true, stop traverse
				return true;

		}

		return false;

	}

	/**
	 * load descendant by id
	 * 
	 * @param id
	 * @return
	 */
	public Tag loadById(final String id) {
		Tag tag = get("id", id);
		if (tag == null)
			throw new TagNotFoundException();

		return tag;
	}

	/**
	 * find first descendant by id
	 * 
	 * @param id
	 * @return
	 */
	public Tag getById(final String id) {
		return get("id", id);
	}

	public List<Tag> find(final String attributeName,
			final String attributeValue, final boolean stopWhenMeetFirst) {
		Map<String, String> attrs = new HashMap<String, String>();
		attrs.put(attributeName, attributeValue);
		return find(attrs, stopWhenMeetFirst);

	}

	/**
	 * find descendant by attributes, return true if all attributes are correct.
	 * 
	 * @param attributes
	 * 
	 * @param stopWhenMeetFirst
	 * @return
	 */
	public List<Tag> find(final Map<String, String> attributes,
			final boolean stopWhenMeetFirst) {
		final List<Tag> results = new ArrayList<Tag>();
		traverse(new Tag.TagProcessor() {

			public boolean process(Tag tag) {
				boolean result = true;
				for (String key : attributes.keySet()) {
					String value = attributes.get(key);
					if (value == null
							|| !value.equalsIgnoreCase(tag.getAttribute(key))) {
						result = false;
						break;
					}
				}

				if (result) {
					logger.debug("findDecendantById ok:" + tag);
					results.add(tag);

					if (stopWhenMeetFirst)
						return true;
				}

				return false;
			}
		});
		return results;
	}

	public Tag loadByAttributes(Map<String, String> attributes) {
		Tag tag = get(attributes);
		if (tag == null)
			throw new TagNotFoundException();

		return tag;
	}

	/**
	 * find first descendant by attribute
	 * 
	 * @param name
	 * @param value
	 * @return
	 */
	public Tag loadByAttribute(final String name, final String value) {
		Tag tag = get(name, value);
		if (tag == null)
			throw new TagNotFoundException("name:" + name + ",value:" + value);

		return tag;
	}

	/**
	 * find first descendant by attribute
	 * 
	 * @param attributeName
	 * @param attributeValue
	 * @return
	 */
	public Tag get(final String attributeName, final String attributeValue) {
		List<Tag> results = find(attributeName, attributeValue, true);
		if (results.size() > 0)
			return results.get(0);
		else
			return null;
	}

	/**
	 * search condition combining. 'AND' logic.
	 * 
	 * @param attributes
	 * @return
	 */
	public Tag get(Map<String, String> attributes) {
		List<Tag> results = find(attributes, true);
		if (results.size() > 0)
			return results.get(0);
		else
			return null;
	}

	/**
	 * include itself
	 * 
	 * @param clazz
	 * @return
	 */
	public Tag getAncesterByClass(Class clazz) {
		logger.debug("search by class:" + this.toHtml());

		if (clazz.isAssignableFrom(getClass()))
			return this;
		else {
			Tag p = (Tag) getParent();

			if (p != null) {

				if (p.getChildren().indexOf(this) < 0)
					throw new RuntimeException(
							"child-parent relation inconsist:" + this);
				return p.getAncesterByClass(clazz);
			} else
				return null;

		}

	}

	public Tag loadAncesterByClass(Class clazz) {
		Tag tag = getAncesterByClass(clazz);
		if (tag == null)
			throw new TagNotFoundException(this.toString() + ",class:"
					+ clazz.getSimpleName());
		return tag;
	}

	/**
	 * find descendant by tag type
	 * 
	 * @param tagType
	 * @param stopWhenMeetFirst
	 * @return
	 */
	public List<Tag> find(final TagType tagType, final boolean stopWhenMeetFirst) {
		final List<Tag> results = new ArrayList<Tag>();
		traverse(new Tag.TagProcessor() {

			public boolean process(Tag tag) {

				if (tag.getTagType() == tagType) {

					results.add(tag);

					if (stopWhenMeetFirst)
						return true;
				}

				return false;
			}
		});
		return results;
	}

	/**
	 * find first descendant by tag type
	 * 
	 * @param tagType
	 * @return
	 */
	public Tag loadByTagType(TagType tagType) {
		Tag tag = getByTagType(tagType);
		if (tag == null)
			throw new TagNotFoundException();

		return tag;
	}

	/**
	 * find first descendant by tag type
	 * 
	 * @param tagType
	 * @return
	 */
	public Tag getByTagType(TagType tagType) {
		List<Tag> results = find(tagType, true);
		if (results.size() == 1)
			return results.get(0);
		else
			return null;
	}

	/**
	 * upgrade from generic Tag object to specific Tag such as Table,TR,TD,etc.
	 * 
	 * @return
	 */
	public Tag upgrade() {

		logger.debug("upgrade:" + this);
		Tag t = getTagType().upgrade(this);

		// if (!isRoot() && t != this)
		replaceSingleNode(t);

		t.checkConsistency();

		return t;
	}

	public boolean isSolo() {
		return tagType.isSolo();
	}

	public Tag loadByJwfId(String jwfId) {
		Tag tag = loadByAttribute(JWF_ID, jwfId);
		// if (tag != null)
		// tag.removeAttribute(JWF_ID);

		return tag;
	}

	public long getHtmlLength() {
		StringWriter sw = new StringWriter();
		try {
			writeHtml(sw);
			return sw.toString().length();
		} catch (IOException e) {

			e.printStackTrace();
			throw new RuntimeException(e);
		}

	}
	
	

	/**
	 * jwfclass is a special attribute. we stop searching inside a tag which has
	 * jwfclass attribute(whatever it is). jwfclass is jwf role name under its
	 * parent jwfcomponent(the nearest ascendant) sometimes a tag act as two
	 * role: one role for parent component, another for sub-component. ie.
	 * jwfclass="item>list", which means as item of parent list, as list of sub
	 * list. do not search itself.
	 * 
	 * @param jwfClass
	 * @return
	 */
	public Tag getByJwfClass(String jwfClass) {

		for (TreeNode tn : getChildren()) {
			Tag t = (Tag) tn;

			// if it has jwfclass, no recursive needed.
			if (t.containsAttribute(JWF_CLASS)) {
				String[] classes = t.getAttribute(JWF_CLASS).split(",");

				if (classes[0].equalsIgnoreCase(jwfClass))
					return t;

			} else {

				Tag result = t.getByJwfClass(jwfClass);

				if (result != null)
					return result;
			}
		}

		return null;
	}

	public Tag loadByJwfClass(String jwfClass) {
		Tag tag = getByJwfClass(jwfClass);
		if (tag == null)
			throw new TagNotFoundException("jwfclass:" + jwfClass);

		return tag;
	}

	public boolean getJwfInclude() {
		String include = (String) getAttribute(JWF_INCLUDE);

		if (include != null && include.equalsIgnoreCase("true"))
			return true;
		else
			return false;
	}

	public static void main2(String[] args) {

		Pattern pAttribute = Pattern
				.compile("(\\w+)=((\\w*)|\"(\\w*)\"|'(\\w*)')");

		String t = "<form a='1' b=\"2\">";
		Matcher m = pAttribute.matcher(t);

		StringBuffer sb = new StringBuffer();
		while (m.find()) {
			System.out.println("\t" + m.groupCount());
			for (int i2 = 0; i2 < m.groupCount(); i2++) {
				System.out.println("\t:" + m.group(i2));

			}
		}
	}
}
